/*
 * Copyright (C) 2004 cologic, blackclaw@parsoma.net
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include "stdinc.h"

#include "ScriptManager.h"
#include "Util.h"
#include "StringTokenizer.h"
#include "Client.h"
#include "UserConnection.h"
#include "ClientManager.h"
#include "DownloadManager.h"
#include "LogManager.h"
#include "Thread.h"
#include <mswsock.h>
#include <cstddef>

ScriptManager* Singleton<ScriptManager>::instance = NULL;

CriticalSection SocketPool::cs;
SocketPool::ConnectBuffers SocketPool::connectBuffers;

SocketPool::SocketPool() {
	start();

	WNDCLASS wndClass;
	wndClass.style = 0;		//because the thing's never going to be visible anyway
	wndClass.lpfnWndProc = SocketPool::DNSWorker;
	wndClass.cbClsExtra = 0;
	wndClass.cbWndExtra = 0;
	wndClass.hInstance = NULL;		//According to MSDN, shouldn't work.
	wndClass.hIcon = NULL;
	wndClass.hCursor = NULL;
	wndClass.hbrBackground = NULL;
	wndClass.lpszMenuName = NULL;
	wndClass.lpszClassName = _T("silly DNS window class");
	::RegisterClass(&wndClass);
	wndDNS = ::CreateWindow(_T("silly DNS window class"), _T("silly DNS window"), 0, 0, 0, 0, 0, 0, 0, 0, 0);

	iocp = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, (ULONG_PTR)0, 0);
	initialized = (iocp != NULL);		//carried over from C. fix.
}

SOCKET SocketPool::AddSocket(SOCKET s) {
	return (::CreateIoCompletionPort((HANDLE)s, iocp, (ULONG_PTR)0, 0) == NULL)?
		(SOCKET)INVALID_HANDLE_VALUE:
		s;
}

int SocketPool::Bind(SOCKET s, short port) {
	struct sockaddr_in service;
	service.sin_family = AF_INET;
	service.sin_addr.s_addr = INADDR_ANY;
	service.sin_port = htons(port);

	return bind(s, (sockaddr*)&service, sizeof(sockaddr_in))?
		SOCKET_ERROR:
		0;
}

int SocketPool::Listen(SOCKET s, short port) {
	return (Bind(s, port) || listen(s, 5))?
		SOCKET_ERROR:
		0;
}

int SocketPool::Accept(SOCKET s, int cookie) {
	LPFN_ACCEPTEX lpfnAcceptEx = NULL;
	GUID GuidAcceptEx = WSAID_ACCEPTEX;
	DWORD hole;

	::WSAIoctl(s, SIO_GET_EXTENSION_FUNCTION_POINTER,
		&GuidAcceptEx, sizeof(GuidAcceptEx),
		&lpfnAcceptEx, sizeof(lpfnAcceptEx),
		&hole,
		NULL, NULL);

	if (!lpfnAcceptEx) return !0;

	SOCKET acceptedSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	const int addrLen = sizeof(struct sockaddr_in)+16;
	OVERLAPPEDPLUS *o = new OVERLAPPEDPLUS(OP_ACCEPT, s, acceptedSocket);
	o->cookie = cookie;

	return !(lpfnAcceptEx(s, acceptedSocket, o->lpOutputBuf, 0,
			addrLen, addrLen, NULL, &(o->ol)) == FALSE && WSAGetLastError() == ERROR_IO_PENDING);
}

void SocketPool::Connect(std::string address, short port, int cookie) {
	ConnectBuffer cb(this, port, cookie);
	HANDLE h = ::WSAAsyncGetHostByName(wndDNS, WM_ASYNCDNS, address.c_str(),
			cb.hostbuf, MAXGETHOSTSTRUCT);
	Lock l(cs);
	connectBuffers[h] = cb;
}

void SocketPool::ConnectRaw(in_addr addr, short port, int cookie) {
	LPFN_CONNECTEX lpfnConnectEx = NULL;
	GUID GuidConnectEx = WSAID_CONNECTEX;
	DWORD hole;

	SOCKET s = NewSocket();
	::WSAIoctl(s, SIO_GET_EXTENSION_FUNCTION_POINTER,
		&GuidConnectEx, sizeof(GuidConnectEx),
		&lpfnConnectEx, sizeof(lpfnConnectEx),
		&hole,
		NULL, NULL);

	if (!lpfnConnectEx) return;

	OVERLAPPEDPLUS *o = new OVERLAPPEDPLUS(OP_CONNECT, s, (SOCKET)INVALID_HANDLE_VALUE);
	o->cookie = cookie;

	Bind(s, 0);
	sockaddr_in sin;
	sin.sin_family = AF_INET;
	sin.sin_addr = addr;
	sin.sin_port = htons(port);
	memset(&sin.sin_zero, 0, 8);
	lpfnConnectEx(s, (sockaddr *)&sin, sizeof(sockaddr_in), NULL, 0, &hole, &o->ol);
}

int SocketPool::Read(SOCKET s) {
	OVERLAPPEDPLUS *o = new OVERLAPPEDPLUS(OP_READ, s, (SOCKET)INVALID_HANDLE_VALUE);
	return ::ReadFile((HANDLE)s, readBuf, RECV_BUFFER_SIZE, &dwBytes, &o->ol);
}

int SocketPool::Write(SOCKET s, const char *buf, int len) {
	OVERLAPPEDPLUS *o = new OVERLAPPEDPLUS(OP_WRITE, s, (SOCKET)INVALID_HANDLE_VALUE, len);

	o->len = len;
	memcpy(o->sendBuf, buf, len);

	return ::WriteFile((HANDLE)s, o->sendBuf, len, &dwBytes, &o->ol);
}

void SocketPool::Close(SOCKET s) {

	LPFN_TRANSMITFILE lpfnTransmitFile = NULL;
	GUID GuidTransmitFile = WSAID_TRANSMITFILE;
	DWORD hole;

	::WSAIoctl(s, SIO_GET_EXTENSION_FUNCTION_POINTER,
		&GuidTransmitFile, sizeof(GuidTransmitFile),
		&lpfnTransmitFile, sizeof(lpfnTransmitFile),
		&hole,
		NULL, NULL);

	if (!lpfnTransmitFile) return;

	OVERLAPPEDPLUS *o = new OVERLAPPEDPLUS(OP_CLOSE, s, (SOCKET)INVALID_HANDLE_VALUE);
	lpfnTransmitFile(s, NULL, 0, 0, &o->ol, NULL, TF_DISCONNECT);
}

int SocketPool::run() {
	while (true) {
		DWORD dwBytesXfered;
		OVERLAPPED *o;
		ULONG_PTR *PerHandleKey;

		if (!GetQueuedCompletionStatus(iocp, &dwBytesXfered, (PULONG_PTR)&PerHandleKey,
			&o, INFINITE)) continue;
		OVERLAPPEDPLUS *op = (OVERLAPPEDPLUS *)o;

		switch (op->OpCode) {
			case OP_ACCEPT:
				AddSocket(op->sClient);
				OnAccept(op->sClient, op->cookie);
				Read(op->sClient);
				Accept(op->s, op->cookie);
				delete op;
				break;
			case OP_CONNECT:
				OnConnect(op->s, op->cookie);
				Read(op->s);
				delete op;
				break;
			case OP_READ:
				if (dwBytesXfered) {
					/* connection is fine. */
					OnRead(op->s, readBuf, dwBytesXfered);
					Read(op->s);
				} else {
					// nope; should clean up outstanding writes/etc.
					OnClose(op->s);
				}
				delete op;
				break;
			case OP_WRITE:
				/* if whole write didn't occur, try again. */
				/* right now, this is probably incorrect - given buffers A & B, can:
					A (partial)
					B
					A (rest)
					happen, requiring me to serialize outputting myself? I've tried to
					artificially induce partial writes before, but to no avail. It seems that
					if one write hits some limit that causes a partial write, the rest should
					too. */
				op->len -= dwBytesXfered;
				op->sendBuf += dwBytesXfered;
				if (op->len > 0) {
					/* partial write occurred. */
					//printf("status: partial write of size %d occurred: len = %d\n", dwBytesXfered, op->len);
					::WriteFile((HANDLE)op->s, op->sendBuf, op->len, &dwBytes, o);
				} else {
					OnWrite(op->s);
					delete op;
				}
				break;
			case OP_CLOSE:
				OnClose(op->s);
				delete op;
				break;
			default:
				break;
		}
	}

	return 0;
}

LRESULT CALLBACK SocketPool::DNSWorker(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
	switch (uMsg) {
		case WM_ASYNCDNS: {
			if (WSAGETASYNCERROR(lParam)) return 0;		//lParam == 0x2afc0000 is DNS not found...
			HANDLE h = (HANDLE)wParam;
			Lock l(cs);
			ConnectBuffer cb = connectBuffers[h];
			hostent *host = (hostent *)(cb.hostbuf);
			cb.context->ConnectRaw(*(in_addr *)host->h_addr_list[0], cb.port, cb.cookie);
			connectBuffers.erase(h);
			return 0;
		} default:
			return ::DefWindowProc(hwnd, uMsg, wParam, lParam);
	}
}

void SocketPool::OnLuaSocketCall(const string &call, SOCKET s) {
	MakeCall("socket", call, 0, s);
}

void SocketPool::OnLuaCookieCall(const string &call, SOCKET s, int cookie) {
	MakeCall("socket", call, 0, s, cookie);
}

void SocketPool::OnRead(SOCKET s, char *buf, int len) {
	MakeCall("socket", "OnRead", 0, s, string(buf, len));
}

static void callalert (lua_State *L, int status) {
	if (status != 0) {
		lua_getglobal(L, "_ALERT");
		if (lua_isfunction(L, -1)) {
			lua_insert(L, -2);
			lua_call(L, 1, 0);
		}
		else {  /* no _ALERT function; print it on stderr */
			ScriptManager::getInstance()->SendDebugMessage(string("LUA ERROR: ") + lua_tostring(L, -2));
			lua_pop(L, 2);  /* remove error message and _ALERT */
		}
	}
}

static int aux_do (lua_State *L, int status) {
  if (status == 0) {  /* parse OK? */
    status = lua_pcall(L, 0, LUA_MULTRET, 0);  /* call main */
  }
  callalert(L, status);
  return status;
}

static int lua_dofile (lua_State *L, const char *filename) {
	return aux_do(L, luaL_loadfile(L, filename));
}

static int lua_dobuffer (lua_State *L, const char *buff, size_t size,
 						const char *name) {
	return aux_do(L, luaL_loadbuffer(L, buff, size, name));
}

static int lua_dostring (lua_State *L, const char *str) {
	return lua_dobuffer(L, str, strlen(str), str);
}

const char LuaManager::className[] = "DC";
Lunar<LuaManager>::RegType LuaManager::methods[] = {
	{"SendHubMessage", &LuaManager::SendHubMessage },
	{"SendClientMessage", &LuaManager::SendClientMessage },
	{"SendUDP", &LuaManager::SendUDPPacket},
	{"PrintDebug", &LuaManager::GenerateDebugMessage},
	{"GetClientIp", &LuaManager::GetClientIp},
	{"GetHubIpPort", &LuaManager::GetHubIpPort},
	{"InjectHubMessage", &LuaManager::InjectHubMessageNMDC},
	{"InjectHubMessageADC", &LuaManager::InjectHubMessageADC},
	{"HubWindowAttention", &LuaManager::HubWindowAttention},
	{"FindWindowHandle", &LuaManager::FindWindow},
	{"SendWindowMessage", &LuaManager::PostMessage},
	{"CreateClient", &LuaManager::CreateClient},
	{"DeleteClient", &LuaManager::DeleteClient},
	{"SocketAccept", &LuaManager::SocketAccept},
	{"SocketConnect", &LuaManager::SocketConnect},
	{"SocketWrite", &LuaManager::SocketWrite},
	{"SocketClose", &LuaManager::SocketClose},
	{"GetSetting", &LuaManager::GetSetting},
	{0}
};

int LuaManager::SocketAccept(lua_State* L) {
	//ss implicit (only one socket group, pre-inited, exposed to lua)
	if (lua_gettop(L) == 2 && lua_isnumber(L, -2) && lua_isnumber(L, -1)) {
		SOCKET s = ScriptManager::getInstance()->ss.NewSocket();
		ScriptManager::getInstance()->ss.Listen(s, static_cast<short>(lua_tonumber(L, -2)));
		ScriptManager::getInstance()->ss.Accept(s, static_cast<int>(lua_tonumber(L, -1)));
		lua_pushnumber(L, s);

		return 1;
	}
	return 0;
}

int LuaManager::SocketConnect(lua_State* L) {
	//ss implicit (only one socket group, pre-inited, exposed to lua)
	if (lua_gettop(L) == 3 && lua_isstring(L, -3) && lua_isnumber(L, -2) && lua_isnumber(L, -1)) {
		ScriptManager::getInstance()->ss.Connect(lua_tostring(L, -3), static_cast<short>(lua_tonumber(L, -2)), static_cast<int>(lua_tonumber(L, -1)));
	}
	return 0;
}

int LuaManager::SocketWrite(lua_State* L) {
	if (lua_gettop(L) == 2 && lua_isnumber(L, -2) && lua_isstring(L, -1)) {
		ScriptManager::getInstance()->ss.Write(static_cast<SOCKET>(lua_tonumber(L, -2)),
			lua_tostring(L, -1), lua_strlen(L, -1));
	}
	return 0;
}

int LuaManager::SocketClose(lua_State* L) {
	if (lua_gettop(L) == 1 && lua_isnumber(L, -1)) {
		ScriptManager::getInstance()->ss.Close(static_cast<SOCKET>(lua_tonumber(L, -1)));
	}
	return 0;
}

int LuaManager::DeleteClient(lua_State* L){
	if (lua_gettop(L) == 1 && lua_islightuserdata(L, -1)){
		Client* client = (Client*) lua_touserdata(L, -1);
		ClientManager::getInstance()->putClient(client);
	}
	return 0;
}

int LuaManager::CreateClient(lua_State* L) {
	if (lua_gettop(L) == 2 && lua_isstring(L, -2) && lua_isstring(L, -1)){
		Client* client = ClientManager::getInstance()->getClient(/*lua_tostring(L, -2)*/);
		client->setNick(lua_tostring(L, -1));
		client->setPassword("");
		client->setDescription("");
		//this will block?
		client->connect();

		lua_pushlightuserdata(L, client);
		return 1;
	}

	return 0;
}

int LuaManager::InjectHubMessageNMDC(lua_State* L) {
	if (lua_gettop(L) == 2 && lua_islightuserdata(L, -2) && lua_isstring(L, -1))
		reinterpret_cast<Client *>(lua_touserdata(L, -2))->onLine(lua_tostring(L, -1));

	return 0;
}

int LuaManager::InjectHubMessageADC(lua_State* L) {
	if (lua_gettop(L) == 2 && lua_islightuserdata(L, -2) && lua_isstring(L, -1))
		reinterpret_cast<Client *>(lua_touserdata(L, -2))->onLine(lua_tostring(L, -1));

	return 0;
}

int LuaManager::HubWindowAttention(lua_State* L) {
	if (lua_gettop(L) == 1 && lua_islightuserdata(L, -1)) {
		reinterpret_cast<Client *>(lua_touserdata(L, -1))->attention();
	}

	return 0;
}

int LuaManager::PostMessage(lua_State* L) {
	if (lua_gettop(L) == 4 && lua_islightuserdata(L, -4) && lua_isnumber(L, -3) && lua_islightuserdata(L, -2) && lua_islightuserdata(L, -1)) {
		::SendMessage(reinterpret_cast<HWND>(lua_touserdata(L, -4)), static_cast<UINT>(lua_tonumber(L, -3)), reinterpret_cast<WPARAM>(lua_touserdata(L, -2)), reinterpret_cast<LPARAM>(lua_touserdata(L, -1)));
	}

	return 0;
}

int LuaManager::FindWindow(lua_State* L) {
	if (lua_gettop(L) == 2 && lua_isstring(L, -2) && lua_isstring(L, -1)) {
		#ifdef UNICODE
			lua_pushlightuserdata(L, ::FindWindow(Text::toT(string(lua_tostring(L, -2))).c_str(), Text::toT(string(lua_tostring(L, -1))).c_str()));
		#else
			lua_pushlightuserdata(L, ::FindWindow(lua_tostring(L, -2), lua_tostring(L, -1)));
		#endif
		return 1;
	}

	return 0;
}

int LuaManager::SendClientMessage(lua_State* L) {
	if (lua_gettop(L) == 2 && lua_islightuserdata(L, -2) && lua_isstring(L, -1)) {
		reinterpret_cast<UserConnection *>(lua_touserdata(L, -2))->sendRaw(lua_tostring(L, -1));
	}

	return 0;
}

int LuaManager::SendHubMessage(lua_State* L) {
	if (lua_gettop(L) == 2 && lua_islightuserdata(L, -2) && lua_isstring(L, -1)) {
		reinterpret_cast<Client*>(lua_touserdata(L, -2))->send(lua_tostring(L, -1));
	}

	return 0;
}

int LuaManager::GenerateDebugMessage(lua_State* L) {
	/* arguments: socket, buffer, address */
	if (lua_gettop(L) == 1 && lua_isstring(L, -1))
		ScriptManager::getInstance()->SendDebugMessage(lua_tostring(L, -1));

	return 0;
}

int LuaManager::SendUDPPacket(lua_State* L) {
	/* arguments: ip:port, data */
	if (lua_gettop(L) == 2 && lua_isstring(L, -2) && lua_isstring(L, -1)) {
		StringList sl = StringTokenizer<string>(lua_tostring(L, -2), ':').getTokens();
		ScriptManager::getInstance()->s.writeTo(sl[0], static_cast<short>(Util::toInt(sl[1])), lua_tostring(L, -1), lua_strlen(L, -1));
	}

	return 0;
}

int LuaManager::GetSetting(lua_State* L) {
	/* arguments: string */
	int n, type;
	n = type = 0;
	if(lua_gettop(L) == 1 && lua_isstring(L, -1) && SettingsManager::getInstance()->getType(lua_tostring(L, -1), n, type)) {
		if(type == SettingsManager::TYPE_STRING) {
			lua_pushstring(L, SettingsManager::getInstance()->get((SettingsManager::StrSetting)n).c_str());
			return 1;
		} else if(type == SettingsManager::TYPE_INT) {
			lua_pushnumber(L, SettingsManager::getInstance()->get((SettingsManager::IntSetting)n));
			return 1;
		} else if(type == SettingsManager::TYPE_INT64) {
			lua_pushnumber(L, static_cast<lua_Number>(SettingsManager::getInstance()->get((SettingsManager::Int64Setting)n)));
			return 1;
		}
	}
	lua_pushliteral(L, "GetSetting: setting not found");
	lua_error(L);
	return 0;
}

int LuaManager::GetClientIp(lua_State* L) {
	/* arguments: client */
	UserConnection* uc = (UserConnection*)lua_touserdata(L, 1);
	if(uc == NULL) {
		lua_pushliteral(L, "GetClientIpPort: missing client pointer");
		lua_error(L);
		return 0;
	}
	lua_pushstring(L, uc->getRemoteIp().c_str());
	return 1;
}

int LuaManager::GetHubIpPort(lua_State* L) {
	/* arguments: client */
	Client* c = (Client*)lua_touserdata(L, 1);
	if(c == NULL) {
		lua_pushliteral(L, "GetHubIpPort: missing hub pointer");
		lua_error(L);
		return 0;
	}
	lua_pushstring(L, c->getIpPort().c_str());
	return 1;
}

lua_State* ScriptInstance::L = 0;		//filled in by scriptmanager.
CriticalSection ScriptInstance::cs;

ScriptManager::ScriptManager() {
}

void ScriptManager::load() {
	L = lua_open();
	luaopen_base(L);
	luaopen_io(L);
	luaopen_string(L);
	luaopen_math(L);
	luaopen_table(L);
	luaopen_debug(L);
	luaopen_loadlib(L);

	Lunar<LuaManager>::Register(L);

	//create default text formatting function, in case startup.lua or formatting.lua isn't present.
	u_int32_t color = SETTING(TEXT_COLOR);
	//this create a dcpp namespace. However, if startup.lua executes, if first clobbers this.
	string function =
			"dcpp = {_init_me_anyway = true}\n"
			"function dcpp.FormatChatText(hub, text)\n"
			"	text = string.gsub(text, \"([{}\\\\])\", \"\\%1\")\n"
			"	text = string.gsub(text, \"\\n\", \"\\\\line\\n\")\n"
			"	return \"{\\\\rtf1\\\\ansi\\\\deff0\\\\plain0\\n\"..\n"
			"			\"{\\\\colortbl ;"
					"\\\\red" + Util::toString(color & 0xFF) +
					"\\\\green" + Util::toString((color >> 8) & 0xFF) +
					"\\\\blue" + Util::toString((color >> 16) & 0xFF) +
					";}\\n\"..\n"
			"			\"\\\\cf1 \"..text..\"}\\n\"\n"
			"end\n";
	lua_dostring(L, function.c_str());

	lua_pop(L, lua_gettop(L));		//hm. starts at 8 or so for me. I have no idea why...

	s.create(Socket::TYPE_UDP);

	TimerManager::getInstance()->addListener(this);
	ClientManager::getInstance()->addListener(this);
}

void ScriptInstance::EvaluateChunk(const string& chunk) {
	Lock l(cs);
	lua_dostring(L, chunk.c_str());
}

void ScriptInstance::EvaluateFile(const string& fn) {
	Lock l(cs);
	lua_dofile(L, (Util::getAppPath() + "scripts\\" + fn).c_str());
}

void ScriptManager::SendDebugMessage(const string &mess) {
	if(File::existsFile(Util::getAppPath() + "scripts\\startup.lua")) {
		fire(ScriptManagerListener::DEBUG_MESSAGE, mess);
		LogManager::getInstance()->message(mess);
	}
}

bool ScriptInstance::GetLuaBool() {
	//get value from top of stack, check if should cancel message.
	bool ret = false;
	if (lua_gettop(L) > 0) {
		ret = !lua_isnil(L, -1);
		lua_pop(L, 1);
	}
	return ret;
}

/*string ScriptInstance::GetClientType(Client* aClient) {
	return dynamic_cast<AdcHub *>(aClient)?"adch":"nmdch";
}*/

void ScriptManager::on(ClientManagerListener::Types type, Client* aClient) throw() {
	if (type == ClientManagerListener::CLIENT_DISCONNECTED) {
		MakeCall("nmdch", "OnHubRemoved", 0, aClient);
	} else if (type == ClientManagerListener::CLIENT_CONNECTED) {
		MakeCall("nmdch", "OnHubAdded", 0, aClient);
	}
}

void ScriptManager::on(Second, u_int32_t /*ticks*/) {
	MakeCall("dcpp", "OnTimer", 0, 0);
}

void ScriptInstance::LuaPush(int i) { lua_pushnumber(L, i); }
void ScriptInstance::LuaPush(const string& s) { lua_pushlstring(L, s.data(), s.size()); }

bool ScriptInstance::MakeCallRaw(const string& table, const string& method, int args, int ret) throw() {
	lua_getglobal(L, table.c_str());		// args + 1
	lua_pushstring(L, method.c_str());		// args + 2
	if (lua_istable(L, -2)) {
		lua_gettable(L, -2);				// args + 2
		lua_remove(L, -2);					// args + 1
		lua_insert(L, 1);					// args + 1
	if(lua_pcall(L, args, ret, 0) == 0) {
			dcassert(lua_gettop(L) == ret);
		return true;
	}
		const char *msg = lua_tostring(L, -1);
		string formatted_msg = (msg != NULL)?string("LUA Error: ") + msg:string("LUA Error: (unknown)");
		ScriptManager::getInstance()->SendDebugMessage(formatted_msg);
		dcassert(lua_gettop(L) == 1);
		lua_pop(L, 1);
	} else {
		lua_settop(L, 0);
	}
	return false;
}

/**
 * @file ScriptManager.cpp
 * $Id: ScriptManager.cpp 262 2005-07-17 20:12:42Z Pothead $
 */